package com.tmx.mvcframework.servlet;

import com.tmx.mvcframework.annotation.*;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;

/*
 ** 该类用来实现一个springmvc效果的类
 */
public class MXDispatcherServlet2 extends HttpServlet {

    private Properties properties = new Properties(); //从加载的properties文件中读取到的对象
    private List<String> classNames  = new ArrayList<>();//用来存放指定包下读取到的类名
    private Map<String,Object> ioc = new HashMap<>();//ioc容器，用来存放bean的容器
    private Map<String,Method> handlerMapping = new HashMap<>();//存放指定url和方法的容器

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req,resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //6，接受请求处理
        try {
            doDispatch(req,resp);
        } catch (Exception e) {
            e.printStackTrace();
            resp.getWriter().write("500 Serve Error!");
        }
    }

    private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        //对请求的url路径进行匹配，唤起相应的方法进行执行
        String uri = req.getRequestURI();
        String contextPath = req.getContextPath();
        uri = uri.replaceAll(contextPath,"").replaceAll("/+","/");
        if (!this.handlerMapping.containsKey(uri)){
            resp.getWriter().write("404,Not Found!");
            return;
        }

        //如何调起方法呢？结果又是怎样进行返回的呢？

        Method method = this.handlerMapping.get(uri);
        String beanName = toLowerFirstWord(method.getDeclaringClass().getSimpleName());

        //获取参数
        Map<String,String[]> parameterMap = req.getParameterMap();

        //怎么才能将多个参数一起传过来呢？现在的方法太写死了，

        //利用反射将方法的形参读出来，得到形参列表
        Class<?>[] parameterTypes = method.getParameterTypes();
        Object[] parameterValues = new Object[parameterTypes.length];//传给method的形参

        for (int i = 0; i < parameterTypes.length; i++) {
            Class<?> parameterType = parameterTypes[i];
            if (parameterType == HttpServletRequest.class){
                parameterValues[i] = req;
            }else if (parameterType == HttpServletResponse.class){
                parameterValues[i] = resp;
            }else if (parameterType == String.class){
                Annotation[][] pa = method.getParameterAnnotations();
                for (Annotation annotations : pa[i]) {//二维数组的遍历
                    if (annotations instanceof MXRequestParam){//这里只考虑了MXRequestParam一种注解的情况
                        String paramName = ((MXRequestParam) annotations).value();
                        if ("".equals(paramName.trim())){continue;}
                        String value = Arrays.toString(parameterMap.get(paramName))
                                .replaceAll("\\[|\\]", "")//去掉中括号【】
                                .replaceAll("\\s", ",");//去掉空格
                        parameterValues[i] = value;
                    }
                }
            }

        }
//        method.invoke(this.ioc.get(beanName),new Object[]{req,resp,parameterMap.get("name")[0]});
        method.invoke(this.ioc.get(beanName),parameterValues);
    }

    @Override
    public void init(ServletConfig config) throws ServletException {
        //1,加载配置文件
        doLoadConfig(config.getInitParameter("contextConfigLocation"));
        //2,扫描相关的类
        doScanner(properties.getProperty("basePackage"));
        //3,初始化扫描到的类，并且放到ioc容器中
        doInstance();
        //4,完成自动化的依赖注入
        doAutowired();
        //5,初始化HandlerMapping
        doInitHandlerMapping();

        System.out.println("MX spring framework is init.");
    }

    private void doInitHandlerMapping() {
        if (ioc.isEmpty()){return;}
        //handMapping 是什么东西？怎么进行定义？
        //是一个获取方法上的url链接的方法
        for (Map.Entry<String, Object> entry : ioc.entrySet()) {
            Class<?> clazz = entry.getValue().getClass();
            String baseurl;
            if (!clazz.isAnnotationPresent(MXRequestMapping.class)){
                continue;
            }else {
                MXRequestMapping mxRequestMapping = clazz.getAnnotation(MXRequestMapping.class);
                baseurl = mxRequestMapping.value();
            }
            for (Method method : entry.getValue().getClass().getMethods()) {
                if (method.isAnnotationPresent(MXRequestMapping.class)){
                    MXRequestMapping mxRequestMapping = method.getAnnotation(MXRequestMapping.class);
                    //如果用户写的路径名不包括/号呢
                    String url = ("/" + baseurl + "/"+ mxRequestMapping.value()).replaceAll("/+","/");
                    handlerMapping.put(url,method);
                    System.out.println("Mapper:" + url + "," + method);
                }
            }
        }
    }

    private void doAutowired() {
        if (ioc.isEmpty()){return;}
        //怎样进行自动依赖呢？有哪些东西需要依赖注入？
        //1，类中标有@MXAutowired、@MXResource的属性，那如何将这两个从类中读取出来呢？读取出来之后又怎样进行赋值呢？
        for (Map.Entry<String, Object> entry : ioc.entrySet()) {
            for (Field field : entry.getValue().getClass().getDeclaredFields()) {//取出对象中的所有属性，包括私有和公有的字段
                if(field.isAnnotationPresent(MXAutowired.class)){
                    MXAutowired mxAutowired = field.getAnnotation(MXAutowired.class);
                    String beanName = mxAutowired.value().trim();
                    //如果注解的值为空，就按照类型进行注入
                    if ("".equals(beanName)){
                        beanName = field.getType().getName();
                    }
                    //开启强制访问
                    field.setAccessible(true);
                    try {
                        field.set(entry.getValue(),ioc.get(beanName));
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                        continue;
                    }
                }

            }
        }
    }

    private void doInstance() {
        if (classNames.isEmpty()){return;}
        //实例化扫描到的类
        for (String className : classNames) {
            try {
                Class<?> clazz = Class.forName(className);
                //将其放入ioc容器中,是以什么为键呢？应该是以类名首字母小写为键
                //如何获得这个类名呢？如果是接口的话该怎么办呢？

                //1,类上面有MXController时，直接将该类交给ioc容器管理 -- 当然也可以自定义beanName的名称
                if (clazz.isAnnotationPresent(MXController.class)){
                    String beanName = toLowerFirstWord(clazz.getSimpleName());
                    if (ioc.containsKey(beanName)){
                        throw new Exception("sorry," + beanName + "is exits");
                    }
                    Object instance = clazz.newInstance();//实例化
                    ioc.put(beanName,instance);
                }else if (clazz.isAnnotationPresent(MXService.class)){
                    //2，类上面有MXService注解时，需要判断下列三种情况：
                    Object instance = clazz.newInstance();//实例化
                    //2.1 注解上是否有值？有值的话就将该值作为ioc容器中的键，否则就用类名首字母小写作为ioc中的键
                    MXService mxService = clazz.getAnnotation(MXService.class);
                    if (!"".equals(mxService.value())){//取出注解上自定义的值
                        ioc.put(mxService.value(),instance);
                    }else {
                        String beanName = toLowerFirstWord(clazz.getSimpleName());//clazz.getSimpleName == 获取类名
                        ioc.put(beanName, instance);
                    }
                    //2.2 属性是否是接口注入的？如果是接口注入的，那么需要将接口的全路径名作为ioc中的key，但仍是实例化出来的这个对象
                    for (Class<?> i : clazz.getInterfaces()) {
                        //如果一个类实现了多个接口，就有可能注入出错，那怎么办？抛出异常即可
                        if (ioc.containsKey(i.getName())){
                            throw new RuntimeException("sorry,the beanName is exits");
                        }
                        ioc.put(i.getName(),instance);
                    }
                }else {//既不是MXController也不是MXService，就跳过生成对象放入ioc容器的步骤。实际上，在spring中还有一个componment注解，该注解是三个注解的父类。
                    continue;
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        //如何实例化呢？已经拿到了包下的所有类，这个时候就只需要将类名从list集合中取出来，利用反射将其实例化即可
    }

    private String toLowerFirstWord(String simpleName) {
        char[] chars = simpleName.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }

    private void doScanner(String basePackage) {
        //怎么挨个扫描到包中的所有类呢？
        //1，从根路径下加载所有的类的全路径
        URL resource = this.getClass().getClassLoader().getResource("/" + basePackage.replaceAll("\\.","/"));
        //相当于类存放包路径
        File classPath = new File(resource.getFile());
        for (File file : classPath.listFiles()) {
            //递归扫描所有的文件夹
            if (file.isDirectory()){
                doScanner(basePackage + file.getName());
            }else {
                //只需要替换class文件就可以了
                if (!file.getName().endsWith(".class")){continue;}
                //找到包下的类 --- 关键代码
                String className = basePackage + "." + file.getName().replace(".class", "");
                classNames.add(className);
            }
        }
    }

    private void doLoadConfig(String contextConfigLocation) {
        //直接从类路径下找到spring主配置文件所在的路径
        // --- this.getClass().getClassLoader()从classpath根目录下加载指定名称的文件，这是因为getClassLoader就会到根目录上
        // 并且直接将其内容读取出来放到Properties对象中去
        InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
        try {
            properties.load(is);
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            if (null != is){
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }


}
